// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats;

import bats.scene.LightingPreset;
import bats.scene.SceneNode;
import bats.ui.Actions;
import bats.ui.BindScreen;
import bats.ui.GameInputMode;
import bats.ui.GameMode;
import com.beust.jcommander.JCommander;
import com.jme3.app.state.ScreenshotAppState;
import com.jme3.math.Vector2f;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeVersion;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.Misc;
import jme3utilities.MyString;
import jme3utilities.nifty.GuiApplication;
import jme3utilities.ui.InputMode;
import jme3utilities.ui.Signals;

/**
 * A GUI application to play the Bats Game. The application's main entry point
 * is here.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Bats
        extends GuiApplication {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger = Logger.getLogger(Bats.class.getName());
    /**
     * application name for the usage message
     */
    final private static String applicationName = "bats";
    /**
     * short name for world-building mode
     */
    final private static String buildModeShortName = "build";
    /**
     * short name for pose mode
     */
    final private static String poseModeShortName = "pose";
    /**
     * short name for simulation mode
     */
    final private static String simModeShortName = "sim";
    /**
     * application name for its window's title bar
     */
    final private static String windowTitle = "Bats Game";
    // *************************************************************************
    // fields
    /**
     * initial lighting preset for all scenes
     */
    private static LightingPreset initialLightingPreset = null;
    /**
     * command-line parameters
     */
    final private static Parameters parameters = new Parameters();
    /**
     * scene for build mode and simulation mode, allocated in createScenes()
     */
    private SceneNode buildSimScene = null;
    /**
     * scene for pose mode, allocated in createScenes()
     */
    private SceneNode poseScene = null;
    // *************************************************************************
    // new methods exposed

    /**
     * Read the initial lighting preset for all scenes which was specified on
     * the command line.
     *
     * @return initial lighting preset for all scenes
     */
    public LightingPreset getInitialLightingPreset() {
        return initialLightingPreset;
    }

    /**
     * Access the command-line parameters.
     *
     * @return the pre-existing instance (not null)
     */
    public static Parameters getParameters() {
        assert parameters != null;
        return parameters;
    }

    /**
     * Test whether the application is in build mode.
     *
     * @return true if build mode is enabled, otherwise false
     */
    public static boolean isBuildMode() {
        GameMode buildMode = GameMode.findMode(buildModeShortName);
        if (buildMode == null) {
            return false;
        }
        return buildMode.isEnabled();
    }

    /**
     * Test whether the application is in simulation mode.
     *
     * @return true if simulation mode is enabled, otherwise false
     */
    public static boolean isSimulationMode() {
        GameMode simMode = GameMode.findMode(simModeShortName);
        if (simMode == null) {
            return false;
        }
        return simMode.isEnabled();
    }

    /**
     * Main entry point for the Bats Game.
     *
     * @param arguments array of command-line arguments (not null)
     */
    public static void main(String[] arguments) {
        /*
         * Mute the chatty loggers found in some imported packages.
         */
        Misc.setLoggingLevels(Level.WARNING);
        /*
         * Set the logging level for this class.
         */
        logger.setLevel(Level.INFO);
        /*
         * Log version strings.
         */
        logger.log(Level.INFO, "jME3-core version is {0}",
                MyString.quote(JmeVersion.getFullName()));
        logger.log(Level.INFO, "jME3-utilities version is {0}",
                MyString.quote(Misc.getVersionShort()));
        /*
         * Instantiate the application.
         */
        Bats application = new Bats();
        /*
         * Parse the command-line arguments into parameters.
         */
        JCommander jCommander = new JCommander(parameters, arguments);
        jCommander.setProgramName(applicationName);
        if (parameters.usageOnly()) {
            jCommander.usage();
            return;
        }
        if (!parameters.consistent()) {
            /*
             * conflicting arguments
             */
            jCommander.usage();
            return;
        }
        String name = parameters.lightingPresetName();
        initialLightingPreset = LightingPreset.fromDescription(name);
        if (initialLightingPreset == null) {
            /*
             * invalid preset name
             */
            jCommander.usage();
            return;
        }
        /*
         * Don't pause on lost focus.  This simplifies debugging and
         * permits the application to keep running while minimized.
         */
        application.setPauseOnLostFocus(false);
        /*
         * Initialize view port settings.
         */
        if (!parameters.settingsDialog()) {
            application.setShowSettings(false);
            /*
             * Override the default window resolution.
             */
            AppSettings settings = new AppSettings(true);
            if (parameters.showHud()) {
                settings.setResolution(1_024, 768);
            } else {
                settings.setResolution(512, 384);
            }
            /*
             * Customize the window's title bar.
             */
            settings.setTitle(windowTitle);
            application.setSettings(settings);
        }

        application.start();
        /*
         * ... and onward to Bats.guiInitializeApplication()!
         */
    }

    /**
     * Process an action which has just begun.
     *
     * @param words action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        String verb = words[0];
        switch (verb) {
            case "go":
                if (words.length == 2) {
                    /*
                     * Switch to a different screen or input mode.
                     */
                    boolean success = goMode(words[1]);
                    if (success) {
                        return true;
                    }
                }
                break;

            case "pick":
                if (words.length == 1) {
                    /*
                     * Get view port coordinates of cursor.
                     */
                    Vector2f clickXY = inputManager.getCursorPosition();
                    /*
                     * Get the status of the shift key.
                     */
                    boolean clearPick = !getSignals().test("shift");
                    /*
                     * Pick the unit at those coordinates.
                     */
                    buildSimScene.pickAt(clickXY, clearPick);
                    return true;
                }
                break;

            case "print": {
                if (words.length == 2) {
                    String what = words[1];
                    if ("root".equals(what)) {
                        new BatsPrinter().printSubtree(rootNode);
                        return true;
                    }
                }
                break;
            }

            case "quit":
                /*
                 * Close the application or popup menu.
                 */
                stop();
                return true;

            case "save": {
                assert words.length > 1;
                String what = words[1];
                switch (what) {
                    case "buildBindings":
                        saveBindings(buildModeShortName);
                        return true;
                    case "poseBindings":
                        saveBindings(poseModeShortName);
                        return true;
                    case "simulationBindings":
                        saveBindings(simModeShortName);
                        return true;
                }
                break;
            }
        }
        return false; // not handled
    }

    /**
     * Save hotkey bindings to a file.
     *
     * @param inputMode which mode to save bindings for (not null)
     */
    public static void saveBindings(InputMode inputMode) {
        try {
            inputMode.saveBindings();
        } catch (IOException exception) {
            logger.log(Level.WARNING,
                    "Output exception while saving hotkey bindings!",
                    exception);
        }
    }
    // *************************************************************************
    // GuiApplication methods

    /**
     * Initialize this application.
     */
    @Override
    public void guiInitializeApplication() {
        /*
         * Initialize assets.
         */
        Assets.setManager(assetManager);
        Assets.setShadingFlag(parameters.shading());
        /*
         * Disable SimpleApplication's FlyByCamera,
         * which is for simple demos, not games.
         */
        flyCam.setEnabled(false);
        /*
         * Disable view frustum culling on the root node and its
         * immediate children.
         */
        rootNode.setCullHint(Spatial.CullHint.Never);
        /*
         * Create two scenes, one for build/sim mode and one for pose mode,
         * each scene having its own view port.
         */
        createScenes();
        /*
         * Initialize the user interface.
         */
        initializeUserInterface();
    }
    // *************************************************************************
    // private methods

    /**
     * Create two scenes, one for build/simulation mode and one for pose mode,
     * each with its own view port.
     */
    private void createScenes() {
        assert buildSimScene == null : buildSimScene;
        assert poseScene == null : poseScene;
        /*
         * Adapt the default view port for build/sim mode.
         */
        assert viewPort.getName().equals("Default") : viewPort.getName();
        assert viewPort.getCamera() == cam : viewPort.getCamera();
        assert viewPort.isClearColor();
        assert viewPort.isClearDepth();
        assert viewPort.isClearStencil();
        viewPort.setEnabled(false);
        assert viewPort.getScenes().size() == 1;
        assert viewPort.getScenes().get(0) == rootNode;
        /*
         * Create a (disabled) secondary view port for pose mode.
         */
        ViewPort poseViewPort = renderManager.createMainView("Pose", cam);
        poseViewPort.setClearFlags(true, true, true);
        poseViewPort.setEnabled(false);
        poseViewPort.attachScene(rootNode);
        /*
         * Create the (disabled) scene for build mode and sim mode.
         */
        String scenarioName = parameters.scenarioName();
        buildSimScene = new SceneNode(this, scenarioName, viewPort);
        buildSimScene.initialize();
        assert !buildSimScene.isEnabled();
        /*
         * Create the (disabled) scene for pose mode.
         */
        poseScene = new SceneNode(this, "pose", poseViewPort);
        poseScene.initialize();
        poseScene.getLighting().getSky().setCloudYOffset(0.5f);
        poseScene.getPov().setKeepVisible(false);
        poseScene.getPov().setMaxRange(20f);
        assert !poseScene.isEnabled();
    }

    /**
     * Switch to the hotkey-binding screen.
     */
    private void goBind() {
        BindScreen bindScreen = stateManager.getState(BindScreen.class);
        assert !bindScreen.isEnabled();
        GameInputMode current = (GameInputMode) InputMode.getEnabledMode();
        bindScreen.activate(current);
    }

    /**
     * Switch to world-building mode.
     */
    private void goBuild() {
        GameMode build = GameMode.findMode(buildModeShortName);
        assert !build.isEnabled();
        GameMode current = GameMode.getEnabledMode();
        current.setEnabled(false);
        buildSimScene.goBuild();
        build.setEnabled(true);
    }

    /**
     * Change the screen or input mode.
     *
     * @param name (short) name of the desired screen or input mode
     * @return true if successful, false if name is invalid
     */
    private boolean goMode(String name) {
        switch (name) {
            case "bind":
                goBind();
                return true;

            case buildModeShortName:
                goBuild();
                return true;

            case poseModeShortName:
                goPose();
                return true;

            case simModeShortName:
                goSimulation();
                return true;
        }
        return false;
    }

    /**
     * Switch to pose mode.
     */
    private static void goPose() {
        GameMode pose = GameMode.findMode(poseModeShortName);
        assert !pose.isEnabled();
        GameMode current = GameMode.getEnabledMode();
        current.setEnabled(false);
        pose.setEnabled(true);
    }

    /**
     * Switch to simulation mode.
     */
    private void goSimulation() {
        GameMode sim = GameMode.findMode(simModeShortName);
        assert !sim.isEnabled();
        GameMode current = GameMode.getEnabledMode();
        current.setEnabled(false);
        buildSimScene.goSimulation();
        sim.setEnabled(true);
    }

    /**
     * Initialize the user interface.
     */
    private void initializeUserInterface() {
        /*
         * Capture a screenshot each time the SYSRQ hotkey is pressed.
         */
        ScreenshotAppState screenShotState = new ScreenshotAppState();
        stateManager.attach(screenShotState);
        /*
         * Disable display of JME statistics.
         * These displays can be re-enabled by pressing the F5 hotkey.
         */
        setDisplayFps(false);
        setDisplayStatView(false);
        /*
         * Instantiate named actions.
         */
        Signals signals = getSignals();
        Actions.initialize(signals);
        /*
         * Hide Nifty warnings about missing controls.
         */
        Logger.getLogger("de.lessvoid.nifty.screen").setLevel(Level.SEVERE);
        /*
         * Disable the default input mode, which is not used by the Bats Game.
         */
        getDefaultInputMode().setEnabled(false);
        /*
         * Instantiate and attach the (disabled) screen controller for editing
         * hotkey bindings.
         */
        BindScreen bindScreen = new BindScreen();
        stateManager.attach(bindScreen);
        /*
         * Instantiate and attach the input mode for worldbuilding.
         */
        boolean showHud = parameters.showHud();
        GameMode buildMode = new GameMode(showHud, buildSimScene,
                buildModeShortName);
        buildMode.setEnabled(parameters.buildMode());
        stateManager.attach(buildMode);
        /*
         * Instantiate and attach the input mode for posing.
         */
        GameMode poseMode = new GameMode(showHud, poseScene, poseModeShortName);
        poseMode.setEnabled(parameters.poseMode());
        poseMode.setPauseStates(parameters.paused(), true);
        stateManager.attach(poseMode);
        /*
         * Instantiate and attach the input mode for simulation.
         */
        GameMode simMode = new GameMode(showHud, buildSimScene,
                simModeShortName);
        boolean enable = !parameters.buildMode() && !parameters.poseMode();
        simMode.setEnabled(enable);
        simMode.setPauseStates(parameters.paused(), false);
        stateManager.attach(simMode);
    }

    /**
     * Save hotkey bindings to a file.
     *
     * @param modeName short name of the input mode whose bindings will be saved
     * (not null)
     */
    private static void saveBindings(String modeName) {
        assert modeName != null;

        InputMode mode = InputMode.findMode(modeName);
        saveBindings(mode);
    }
}